21. State


Posted by WayneCheng on 2021-01-28

關於 State 本篇將討論以下幾個問題

1. 關於 State

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 State

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

by Gang of Four

  • 當物件的內部狀態改變時,允許改變其行為
  • 物件看起來像改變了類別

State(狀態)屬於行為型(Behavioral Patterns),當遇到大量 if/else 或 switch 時,可藉由 State 來將判斷邏輯抽離原本物件,並將每個狀態邏輯拆分開來,各自處理單一狀態與邏輯,降低物件本身複雜度。

優點:

  • 符合 單一職責原則(Single Responsibility Principle)
  • 符合 開閉原則(Open Closed Principle)

缺點:

  • 隨著狀態數量提高,class 數量增加會造成整體程式複雜度提升

2. UML

Class 間關聯:

  • Context 可包含 State
  • ConcreteState A/B 繼承 State

Class:

  • Context:需要判斷狀態來運作的物件
  • State:狀態的抽象類別或介面
  • ConcreteState:狀態實做

3. 將 UML 轉為程式碼

需要判斷狀態來運作的物件

/// <summary>
/// 需要判斷狀態來運作的物件
/// </summary>
public class Context
{
    private IState _state;

    public Context(IState state)
    {
        State = state;
    }

    public IState State
    {
        get => _state;
        set
        {
            _state = value;
            Console.WriteLine($"State: {_state.GetType().Name}");
        }
    }

    public void Request()
    {
        _state.Handle(this);
    }
}

狀態的介面

/// <summary>
/// 狀態的介面
/// </summary>
public interface IState
{
    void Handle(Context context);
}

狀態實做

/// <summary>
/// 狀態實做 A
/// </summary>
public class ConcreteStateA : IState
{
    public void Handle(Context context)
    {
        context.State = new ConcreteStateB();
    }
}

/// <summary>
/// 狀態實做 B
/// </summary>
public class ConcreteStateB : IState
{
    public void Handle(Context context)
    {
        context.State = new ConcreteStateA();
    }
}
  1. 建立context並給定初始狀態
  2. 依據當前狀態呼叫狀態實做
static void Main(string[] args)
{
    Default.Context context = new Default.Context(new Default.ConcreteStateA());

    context.Request();
    context.Request();
    context.Request();
    context.Request();

    Console.ReadLine();
}

執行結果

State: ConcreteStateA
State: ConcreteStateB
State: ConcreteStateA
State: ConcreteStateB
State: ConcreteStateA

4. 情境

我們接到了一個餐飲部麵包店看板要隨著時間改變內容的需求

  • 早上十點到下午兩點「麵包出爐」
  • 下午兩點到八點「麵包搭配咖啡打八折」
  • 晚上八點到九點「麵包全面七折」
  • 其餘時間不營業

狀態的介面

/// <summary>
/// 狀態的介面
/// </summary>
public interface IState
{
    void Handle(BreadStore breadStore, int time);
}

狀態實做,處理每個時間區間狀態

/// <summary>
/// 還沒營業喔
/// </summary>
public class Close : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time < 10 || time > 21)
        {
            Console.WriteLine($"現在時間:{time} 點, 還沒營業喔~");
        }
        else
        {
            breadStore.State = new FreshBread();
            breadStore.Request(time);
        }
    }
}

/// <summary>
/// 麵包出爐囉
/// </summary>
public class FreshBread : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time >= 10 && time < 14)
        {
            Console.WriteLine($"現在時間:{time} 點, 麵包出爐囉~");
        }
        else
        {
            breadStore.State = new BreadAndCoffee();
            breadStore.Request(time);
        }
    }
}

/// <summary>
/// 麵包搭配咖啡打八折
/// </summary>
public class BreadAndCoffee : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time >= 14 && time < 20)
        {
            Console.WriteLine($"現在時間:{time} 點, 麵包搭配咖啡打八折喔~");
        }
        else
        {
            breadStore.State = new ClearingSale();
            breadStore.Request(time);
        }
    }
}

/// <summary>
/// 麵包全面七折喔
/// </summary>
public class ClearingSale : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time >= 20)
        {
            Console.WriteLine($"現在時間:{time} 點, 麵包全面七折喔~");
        }
        else
        {
            breadStore.State = new Close();
            breadStore.Request(time);
        }
    }
}

麵包店

/// <summary>
/// 麵包店
/// </summary>
public class BreadStore
{
    public IState State { get; set; }

    public BreadStore(IState state)
    {
        State = state;
    }

    public void Request(int time)
    {
        State.Handle(this, time);
    }
}
  1. 建立breadStore並給定初始狀態
  2. 依據當前狀態呼叫狀態實做
static void Main(string[] args)
{
    Situation.BreadStore breadStore = new Situation.BreadStore(new Situation.Close());

    breadStore.Request(9);
    breadStore.Request(10);
    breadStore.Request(13);
    breadStore.Request(14);
    breadStore.Request(20);

    Console.ReadLine();
}

執行結果

現在時間:9 點, 還沒營業喔~
現在時間:10 點, 麵包出爐囉~
現在時間:13 點, 麵包出爐囉~
現在時間:14 點, 麵包搭配咖啡打八折喔~
現在時間:20 點, 麵包全面七折喔~

完整程式碼

GitHub:Behavioral_08_State


總結

一開始處理到 if/else 或 switch 判斷數量不多時,不用急著套用 State 來抽離判斷部分邏輯,少量的 if/else 或 switch 在維護上還是相對容易的,且未來若是沒有新增判斷的需求,那這部分反而是過度設計了。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

550. Game Play Analysis IV

550. Game Play Analysis IV

How to Launch an Amazon EC2 Instance

How to Launch an Amazon EC2 Instance

初探 Fetch

初探 Fetch


Comments